/**********************************************************************

  Audacity: A Digital Audio Editor

  Benchmark.cpp

  Dominic Mazzoni

*******************************************************************//**

\class BenchmarkDialog
\brief BenchmarkDialog is used for measuring performance and accuracy
of sample block storage.

*//*******************************************************************/

#include "Benchmark.h"

#include <wx/app.h>
#include <wx/log.h>
#include <wx/textctrl.h>
#include <wx/checkbox.h>
#include <wx/choice.h>
#include <wx/stattext.h>
#include <wx/stopwatch.h>
#include <wx/valgen.h>
#include <wx/valtext.h>

#include "Project.h"
#include "ProjectTimeSignature.h"
#include "SampleBlock.h"
#include "ShuttleGui.h"
#include "TempoChange.h"
#include "WaveClip.h"
#include "WaveTrack.h"
#include "Sequence.h"
#include "Prefs.h"
#include "ProjectRate.h"

#include "FileNames.h"
#include "SelectFile.h"
#include "AudacityMessageBox.h"
#include "wxPanelWrapper.h"

// Change these to the desired format...should probably make the
// choice available in the dialog
#define SampleType short
#define SampleFormat int16Sample

class BenchmarkDialog final : public wxDialogWrapper
{
public:
    // constructors and destructors
    BenchmarkDialog(wxWindow* parent, AudacityProject& project);

    void MakeBenchmarkDialog();

private:
    // WDR: handler declarations
    void OnRun(wxCommandEvent& event);
    void OnSave(wxCommandEvent& event);
    void OnClear(wxCommandEvent& event);
    void OnClose(wxCommandEvent& event);

    void Printf(const TranslatableString& str);
    void HoldPrint(bool hold);
    void FlushPrint();

    AudacityProject& mProject;
    const ProjectRate& mRate;

    bool mHoldPrint;
    wxString mToPrint;

    wxString mBlockSizeStr;
    wxString mDataSizeStr;
    wxString mNumEditsStr;
    wxString mRandSeedStr;

    bool mBlockDetail;
    bool mEditDetail;

    wxTextCtrl* mText;

private:
    DECLARE_EVENT_TABLE()
};

void RunBenchmark(wxWindow* parent, AudacityProject& project)
{
    /*
    int action = AudacityMessageBox(
 XO("This will close all project windows (without saving)\nand open the Audacity Benchmark dialog.\n\nAre you sure you want to do this?"),
       XO("Benchmark"),
       wxYES_NO | wxICON_EXCLAMATION,
       NULL);

    if (action != wxYES)
       return;

    for ( auto pProject : AllProjects{} )
       GetProjectFrame( *pProject ).Close();
    */

    BenchmarkDialog dlog{ parent, project };

    dlog.CentreOnParent();

    dlog.ShowModal();
}

//
// BenchmarkDialog
//

enum {
    RunID = 1000,
    BSaveID,
    ClearID,
    StaticTextID,
    BlockSizeID,
    DataSizeID,
    NumEditsID,
    RandSeedID
};

BEGIN_EVENT_TABLE(BenchmarkDialog, wxDialogWrapper)
EVT_BUTTON(RunID,   BenchmarkDialog::OnRun)
EVT_BUTTON(BSaveID,  BenchmarkDialog::OnSave)
EVT_BUTTON(ClearID, BenchmarkDialog::OnClear)
EVT_BUTTON(wxID_CANCEL, BenchmarkDialog::OnClose)
END_EVENT_TABLE()

BenchmarkDialog::BenchmarkDialog(
    wxWindow* parent, AudacityProject& project)
/* i18n-hint: Benchmark means a software speed test */ :  wxDialogWrapper(parent, 0, XO("Benchmark"),
                                                                          wxDefaultPosition, wxDefaultSize,
                                                                          wxDEFAULT_DIALOG_STYLE
                                                                          | wxRESIZE_BORDER)
    , mProject(project)
    , mRate{ProjectRate::Get(project)}
{
    SetName();

    mBlockSizeStr = wxT("64");
    mNumEditsStr = wxT("100");
    mDataSizeStr = wxT("32");
    mRandSeedStr = wxT("234657");

    mBlockDetail = false;
    mEditDetail = false;

    HoldPrint(false);

    MakeBenchmarkDialog();
}

// WDR: handler implementations for BenchmarkDialog

void BenchmarkDialog::OnClose(wxCommandEvent& WXUNUSED(event))
{
    EndModal(0);
}

void BenchmarkDialog::MakeBenchmarkDialog()
{
    ShuttleGui S(this, eIsCreating);

    // Strings don't need to be translated because this class doesn't
    // ever get used in a stable release.

    S.StartVerticalLay(true);
    {
        S.SetBorder(8);
        S.StartMultiColumn(4);
        {
            //
            S.Id(BlockSizeID)
            .Validator<wxTextValidator>(wxFILTER_NUMERIC, &mBlockSizeStr)
            .AddTextBox(XXO("Disk Block Size (KB):"),
                        wxT(""),
                        12);

            //
            S.Id(NumEditsID)
            .Validator<wxTextValidator>(wxFILTER_NUMERIC, &mNumEditsStr)
            .AddTextBox(XXO("Number of Edits:"),
                        wxT(""),
                        12);

            //
            S.Id(DataSizeID)
            .Validator<wxTextValidator>(wxFILTER_NUMERIC, &mDataSizeStr)
            .AddTextBox(XXO("Test Data Size (MB):"),
                        wxT(""),
                        12);

            ///
            S.Id(RandSeedID)
            .Validator<wxTextValidator>(wxFILTER_NUMERIC, &mRandSeedStr)
            /* i18n-hint: A "seed" is a number that initializes a
               pseudorandom number generating algorithm */
            .AddTextBox(XXO("Random Seed:"),
                        wxT(""),
                        12);
        }
        S.EndMultiColumn();

        //
        S.Validator<wxGenericValidator>(&mBlockDetail)
        .AddCheckBox(XXO("Show detailed info about each block file"),
                     false);

        //
        S.Validator<wxGenericValidator>(&mEditDetail)
        .AddCheckBox(XXO("Show detailed info about each editing operation"),
                     false);

        //
        mText = S.Id(StaticTextID)
                /* i18n-hint noun */
                .Name(XO("Output"))
                .Style(wxTE_MULTILINE | wxTE_READONLY | wxTE_RICH)
                .MinSize({ 500, 200 })
                .AddTextWindow(wxT(""));

        //
        S.SetBorder(10);
        S.StartHorizontalLay(wxALIGN_LEFT | wxEXPAND, false);
        {
            S.StartHorizontalLay(wxALIGN_LEFT, false);
            {
                S.Id(RunID).AddButton(XXO("Run"), wxALIGN_CENTRE, true);
                S.Id(BSaveID).AddButton(XXO("Save"));
                /* i18n-hint verb; to empty or erase */
                S.Id(ClearID).AddButton(XXO("Clear"));
            }
            S.EndHorizontalLay();

            S.StartHorizontalLay(wxALIGN_CENTER, true);
            {
                // Spacer
            }
            S.EndHorizontalLay();

            S.StartHorizontalLay(wxALIGN_NOT | wxALIGN_LEFT, false);
            {
                /* i18n-hint verb */
                S.Id(wxID_CANCEL).AddButton(XXO("Close"));
            }
            S.EndHorizontalLay();
        }
        S.EndHorizontalLay();
    }
    S.EndVerticalLay();

    Fit();
    SetSizeHints(GetSize());
}

void BenchmarkDialog::OnSave(wxCommandEvent& WXUNUSED(event))
{
/* i18n-hint: Benchmark means a software speed test;
   leave untranslated file extension .txt */
    auto fName = XO("benchmark.txt").Translation();

    fName = SelectFile(FileNames::Operation::Export,
                       XO("Export Benchmark Data as:"),
                       wxEmptyString,
                       fName,
                       wxT("txt"),
                       { FileNames::TextFiles },
                       wxFD_SAVE | wxRESIZE_BORDER,
                       this);

    if (fName.empty()) {
        return;
    }

    mText->SaveFile(fName);
}

void BenchmarkDialog::OnClear(wxCommandEvent& WXUNUSED(event))
{
    mText->Clear();
}

void BenchmarkDialog::Printf(const TranslatableString& str)
{
    auto s = str.Translation();
    mToPrint += s;
    if (!mHoldPrint) {
        FlushPrint();
    }
}

void BenchmarkDialog::HoldPrint(bool hold)
{
    mHoldPrint = hold;

    if (!mHoldPrint) {
        FlushPrint();
    }
}

void BenchmarkDialog::FlushPrint()
{
    while (mToPrint.length() > 100) {
        mText->AppendText(mToPrint.Left(100));
        mToPrint = mToPrint.Right(mToPrint.length() - 100);
    }
    if (mToPrint.length() > 0) {
        mText->AppendText(mToPrint);
    }
    mToPrint = wxT("");
}

void BenchmarkDialog::OnRun(wxCommandEvent& WXUNUSED(event))
{
    TransferDataFromWindow();

    if (!Validate()) {
        return;
    }

    // This code will become part of libaudacity,
    // and this class will be phased out.
    long blockSize, numEdits, dataSize, randSeed;

    mBlockSizeStr.ToLong(&blockSize);
    mNumEditsStr.ToLong(&numEdits);
    mDataSizeStr.ToLong(&dataSize);
    mRandSeedStr.ToLong(&randSeed);

    if (blockSize < 1 || blockSize > 1024) {
        AudacityMessageBox(
            XO("Block size should be in the range 1 - 1024 KB."));
        return;
    }

    if (numEdits < 1 || numEdits > 10000) {
        AudacityMessageBox(
            XO("Number of edits should be in the range 1 - 10000."));
        return;
    }

    if (dataSize < 1 || dataSize > 2000) {
        AudacityMessageBox(
            XO("Test data size should be in the range 1 - 2000 MB."));
        return;
    }

    SettingScope scope;
    EditClipsCanMove.Write(false);

    // Remember the old blocksize, so that we can restore it later.
    auto oldBlockSize = Sequence::GetMaxDiskBlockSize();
    Sequence::SetMaxDiskBlockSize(blockSize * 1024);

    const auto cleanup = finally([&] {
        Sequence::SetMaxDiskBlockSize(oldBlockSize);
    });

    wxBusyCursor busy;

    HoldPrint(true);

    const auto t
        =WaveTrackFactory{ mRate,
                           SampleBlockFactory::New(mProject) }
    .Create(SampleFormat, mRate.GetRate());
    const auto tmp0 = TrackList::Temporary(nullptr, t);
    const auto tempo = ProjectTimeSignature::Get(mProject).GetTempo();
    DoProjectTempoChange(*t, tempo);

    t->SetRate(1);

    srand(randSeed);

    uint64_t nChunks, chunkSize;
    //chunkSize = 7500ull + (rand() % 1000ull);
    chunkSize = 200ull + (rand() % 100ull);
    nChunks = (dataSize * 1048576ull) / (chunkSize * sizeof(SampleType));
    while (nChunks < 20 || chunkSize > (blockSize * 1024) / 4)
    {
        chunkSize = std::max(uint64_t(1), (chunkSize / 2) + (rand() % 100));
        nChunks = (dataSize * 1048576ull) / (chunkSize * sizeof(SampleType));
    }

    // The chunks are the pieces we move around in the test.
    // They are (and are supposed to be) a different size to
    // the blocks that make the sample blocks.  That way we get to
    // do some testing of when edit chunks cross sample block boundaries.
    Printf(XO("Using %lld chunks of %lld samples each, for a total of %.1f MB.\n")
           .Format(nChunks, chunkSize, nChunks * chunkSize * sizeof(SampleType) / 1048576.0));

    int trials = numEdits;

    using Samples = ArrayOf<SampleType>;
    Samples small1{ nChunks };
    Samples block{ chunkSize };

    Printf(XO("Preparing...\n"));

    wxTheApp->Yield();
    FlushPrint();

    int v;
    int bad;
    int z;
    long elapsed;
    wxString tempStr;
    wxStopWatch timer;

    for (uint64_t i = 0; i < nChunks; i++) {
        v = SampleType(rand());
        small1[i] = v;
        for (uint64_t b = 0; b < chunkSize; b++) {
            block[b] = v;
        }

        t->Append(0, (samplePtr)block.get(), SampleFormat, chunkSize);
    }
    t->Flush();

    // This forces the WaveTrack to flush all of the appends (which is
    // only necessary if you want to access the Sequence class directly,
    // as we're about to do).
    t->GetEndTime();

    if (t->GetClip(0)->GetVisibleSampleCount() != nChunks * chunkSize) {
        Printf(XO("Expected len %lld, track len %lld.\n")
               .Format(
                   nChunks * chunkSize,
                   t->GetClip(0)->GetVisibleSampleCount()
                   .as_long_long()));
        goto fail;
    }

    Printf(XO("Performing %d edits...\n").Format(trials));
    wxTheApp->Yield();
    FlushPrint();

    timer.Start();
    for (z = 0; z < trials; z++) {
        // First chunk to cut
        // 0 <= x0 < nChunks
        const uint64_t x0 = rand() % nChunks;

        // Number of chunks to cut
        // 1 <= xlen <= nChunks - x0
        const uint64_t xlen = 1 + (rand() % (nChunks - x0));
        if (mEditDetail) {
            Printf(XO("Cut: %lld - %lld \n")
                   .Format(x0 * chunkSize, (x0 + xlen) * chunkSize));
        }

        Track::Holder tmp;
        try {
            tmp
                =t->Cut(double(x0 * chunkSize), double((x0 + xlen) * chunkSize));
        }
        catch (const AudacityException&) {
            Printf(XO("Trial %d\n").Format(z));
            Printf(XO("Cut (%lld, %lld) failed.\n")
                   .Format((x0 * chunkSize), (x0 + xlen) * chunkSize));
            Printf(XO("Expected len %lld, track len %lld.\n")
                   .Format(
                       nChunks * chunkSize,
                       t->GetClip(0)->GetVisibleSampleCount()
                       .as_long_long()));
            goto fail;
        }

        // Position to paste
        // 0 <= y0 <= nChunks - xlen
        const uint64_t y0 = rand() % (nChunks - xlen + 1);

        if (mEditDetail) {
            Printf(XO("Paste: %lld\n").Format(y0 * chunkSize));
        }

        try {
            t->Paste((double)(y0 * chunkSize), *tmp);
        }
        catch (const AudacityException&) {
            Printf(XO("Trial %d\nFailed on Paste.\n").Format(z));
            goto fail;
        }

        if (t->GetClip(0)->GetVisibleSampleCount() != nChunks * chunkSize) {
            Printf(XO("Trial %d\n").Format(z));
            Printf(XO("Expected len %lld, track len %lld.\n")
                   .Format(
                       nChunks * chunkSize,
                       t->GetClip(0)->GetVisibleSampleCount()
                       .as_long_long()));
            goto fail;
        }

        // Permute small1 correspondingly to the cut and paste
        auto first = &small1[0];
        if (x0 + xlen < nChunks) {
            std::rotate(first + x0, first + x0 + xlen, first + nChunks);
        }
        std::rotate(first + y0, first + nChunks - xlen, first + nChunks);
    }

    elapsed = timer.Time();

    if (mBlockDetail) {
        // One remaining old direct use of narrow clips, only for debugging
        auto seq = t->GetClip(0)->GetSequence(0);
        seq->DebugPrintf(seq->GetBlockArray(), seq->GetNumSamples(), &tempStr);
        mToPrint += tempStr;
    }
    Printf(XO("Time to perform %d edits: %ld ms\n").Format(trials, elapsed));
    FlushPrint();
    wxTheApp->Yield();

#if 0
    Printf(XO("Checking file pointer leaks:\n"));
    Printf(XO("Track # blocks: %ld\n").Format(t->GetBlockArray()->size()));
    Printf(XO("Disk # blocks: \n"));
    system("ls .audacity_temp/* | wc --lines");
#endif

    Printf(XO("Doing correctness check...\n"));
    FlushPrint();
    wxTheApp->Yield();

    bad = 0;
    timer.Start();
    for (uint64_t i = 0; i < nChunks; i++) {
        v = small1[i];
        auto pBlock = reinterpret_cast<samplePtr>(block.get());
        constexpr auto backwards = false;
        t->DoGet(0, 1, &pBlock, SampleFormat, i * chunkSize, chunkSize, backwards);
        for (uint64_t b = 0; b < chunkSize; b++) {
            if (block[b] != v) {
                bad++;
                if (bad < 10) {
                    Printf(XO("Bad: chunk %lld sample %lld\n").Format(i, b));
                }
                b = chunkSize;
            }
        }
    }
    if (bad == 0) {
        Printf(XO("Passed correctness check!\n"));
    } else {
        Printf(XO("Errors in %d/%lld chunks\n").Format(bad, nChunks));
    }

    elapsed = timer.Time();

    Printf(XO("Time to check all data: %ld ms\n").Format(elapsed));
    Printf(XO("Reading data again...\n"));

    wxTheApp->Yield();
    FlushPrint();

    timer.Start();

    for (uint64_t i = 0; i < nChunks; i++) {
        v = small1[i];
        auto pBlock = reinterpret_cast<samplePtr>(block.get());
        constexpr auto backwards = false;
        t->DoGet(0, 1, &pBlock, SampleFormat, i * chunkSize, chunkSize, backwards);
        for (uint64_t b = 0; b < chunkSize; b++) {
            if (block[b] != v) {
                bad++;
            }
        }
    }

    elapsed = timer.Time();

    Printf(XO("Time to check all data (2): %ld ms\n").Format(elapsed));

    Printf(XO("At 44100 Hz, %d bytes per sample, the estimated number of\n simultaneous tracks that could be played at once: %.1f\n")
           .Format(SAMPLE_SIZE(SampleFormat), (nChunks * chunkSize / 44100.0) / (elapsed / 1000.0)));

    goto success;

fail:
    Printf(XO("TEST FAILED!!!\n"));

success:

    Printf(XO("Benchmark completed successfully.\n"));
    HoldPrint(false);
}
